package ru.hh.team02.chat.server.client;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

import ru.hh.team02.chat.server.Client;

public class ClientRegistry {
	static private Map<String, HashSetWithLock<Client>> allClientsByRoom = new HashMap<String, HashSetWithLock<Client>>();
	static private Set<String> allClientNames = new HashSetWithLock<String>();
	static private ReadWriteLock allClientNamesSetLock = new ReentrantReadWriteLock();

	static {
		// Add rooms
		String[] names = { "Room1", "Room2", "Room3" };
		for (String name : names) {
			addNewRoom(name);
		}

	}

	/**
	 * HashSet, but with lock, which can be acquired by getLock()
	 * 
	 * @param <T>
	 */
	@SuppressWarnings("serial")
	private static class HashSetWithLock<T> extends HashSet<T> {
		// adding a lock per Set to be able to lock them separately
		private final ReadWriteLock lock = new ReentrantReadWriteLock();

		public ReadWriteLock getLock() {
			return lock;
		}
	}

	/**
	 * Add a new chat room
	 * 
	 * @param name
	 *            Name of the room
	 * @return <code>true</code> if the room has been added; <code>false</code>
	 *         if the room already exists
	 */
	static private boolean addNewRoom(String name) {
		if (allClientsByRoom.containsKey(name)) {
			return false;
		} else {
			allClientsByRoom.put(name, new HashSetWithLock<Client>());
			return true;
		}
	}

	/**
	 * Get a shallow copy of the set with clients containing in the room.
	 * This is expected to be the most often used method, so readLock will accept
	 * simultaneous access to the data when no writing is performed (which will
	 * be much less frequent and will only happen on client entering chat, exiting and
	 * changing rooms).
	 * 
	 * @param name
	 *            Room name to take the clients from
	 * @return Set of clients.
	 */
	@SuppressWarnings("unchecked")
	public static Set<Client> getClientsFromRoom(String name) {
		if (!allClientsByRoom.containsKey(name)) {
			return null; // no such room
		}
		HashSetWithLock<Client> clients = allClientsByRoom.get(name);
		try {
			clients.getLock().readLock().lock();
			return (Set<Client>) clients.clone();
		} finally {
			clients.getLock().readLock().unlock();
		}

	}

	/**
	 * Add client and register it.
	 * 
	 * @param client
	 *            A client to be registered
	 * @param room
	 *            Room the client will be in.
	 * @return <code>true</code> if client has been successfully registered;
	 *         <code>false</code> if client hasn't been registered due to
	 *         existing a client with the same name or not having the room with
	 *         the name provided.
	 */
	public static boolean addClient(Client client, String room) {
		if (allClientNames.contains(client.getName())) {
			return false;
		}
		if (!hasRoom(room)) {
			return false;
		}

		HashSetWithLock<Client> clientsFromRoom = allClientsByRoom.get(room);
		try {
			// Order of lock application must be preserved in all methods, otherwise we may have a deadlock
			allClientNamesSetLock.writeLock().lock();
			clientsFromRoom.getLock().writeLock().lock();

			allClientNames.add(client.getName());
			clientsFromRoom.add(client);

		} finally {
			clientsFromRoom.getLock().writeLock().unlock();
			allClientNamesSetLock.writeLock().unlock();
		}
		return true;
	}
	
	/**
	 * Unregister the client.
	 * 
	 * @param client
	 *            A client to unregister.
	 * @return <code>true</code> if client has been successfully unregistered;
	 *         <code>false</code> if client is not among clients or haven't been
	 *         found inside their room.
	 */
	public static boolean deleteClient(Client client) {
		HashSetWithLock<Client> clientsFromRoom = allClientsByRoom.get(
				client.getRoom());
		try {
			// Order of lock application must be preserved in all methods, otherwise we may have a deadlock
			allClientNamesSetLock.writeLock().lock();
			clientsFromRoom.getLock().writeLock().lock();

			if (!clientsFromRoom.remove(client)) {
				return false;
			}
			if (!allClientNames.remove(client.getName())) { // this situation should happen as it violates the invariants of this class
				return false;
			}

		} finally {
			clientsFromRoom.getLock().writeLock().unlock();
			allClientNamesSetLock.writeLock().unlock();
		}
		return true;
	}

	public static boolean hasRoom(String room) {
		return allClientsByRoom.containsKey(room);
	}
	
	/**
	 * Change room of the client.
	 * @param client Client to change room for.
	 * @param oldRoom The name of the room the client is currently in.
	 * @param newRoom The name of the room to change to.
	 * @return <code>true</code> if the room has been successfully changed;
	 *         <code>false</code> if the newRoom with the name provided doesn't exist
	 *         or if client is not in the oldRoom.
	 */
	public static boolean changeRoom(Client client, String oldRoom, String newRoom) {
		if (!hasRoom(newRoom) || !hasRoom(oldRoom)) {
			return false;
		}
		
		HashSetWithLock<Client> clientsFromOldRoom = allClientsByRoom.get(client.getRoom());
		HashSetWithLock<Client> clientsFromNewRoom = allClientsByRoom.get(newRoom);
		try {
			// Order of lock application must be preserved in all methods, otherwise we may have a deadlock
			clientsFromOldRoom.getLock().writeLock().lock();
			clientsFromNewRoom.getLock().writeLock().lock();

			clientsFromOldRoom.remove(client);
			clientsFromNewRoom.add(client);

		} finally {
			clientsFromNewRoom.getLock().writeLock().unlock();
			clientsFromOldRoom.getLock().writeLock().unlock();
		}
		return true;
		
	}
}
